package cn.qu.v1.platform.inject;

import cn.qu.v1.platform.aop.ProxyMethod;
import cn.qu.v1.platform.aop.annotation.Aop;
import cn.qu.v1.platform.inject.annotation.*;
import cn.qu.v1.platform.inject.factory.FactoryBean;
import cn.qu.v1.platform.inject.factory.PluginFactoryBean;
import cn.qu.v1.platform.inject.factory.ProxyFactoryBean;
import cn.qu.v1.platform.plugin.Plugin;
import cn.qu.v1.platform.plugin.PluginException;
import cn.qu.v1.platform.plugin.annotation.ServiceStart;
import com.google.common.base.Strings;
import com.google.common.reflect.TypeToken;
import org.apache.commons.collections.comparators.ComparableComparator;
import org.apache.commons.collections.map.HashedMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.*;

/**
 * Created by zh on 17/1/7.
 */
public class BeanContext {

    private static final Logger logger = LoggerFactory.getLogger(BeanContext.class);

    private Map<String, Bucket> bucketMap = new HashMap<>();

    private static BeanContext instance;

    private static Object lock = new Object();

    private BeanContext() {

    }

    public static BeanContext create() {
        synchronized (lock) {
            if (instance == null) {
                instance = new BeanContext();
            }
        }
        return instance;
    }

    public void load(Plugin plugin) throws BeanCreateException {
        Plugin.Meta meta = plugin.getMeta();

        String name = meta.getPluginName();
        int version = meta.getVersionCode();

        String key = Bucket.keyMaker(name, version);
        boolean exists = bucketMap.containsKey(key);
        if (exists) {
            throw new BeanCreateException(MessageFormat.format("{0}已存在，版本：{1}，重新加载前请先卸载。", name, version));
        }

        try {
            Optional<Class> opt = plugin.loadPlugin().loadClass(meta.getEnterClass());
            if (opt.isPresent()) {
                Bucket bucket = new Bucket();
                bucket.setPlugin(plugin);
                bucket.setName(key);

                List<Class<?>> classList = new ArrayList<>();
                getImportClass(classList, opt.get());
                for (Class clazz : classList) {
                    bucket.getConfMap().put(clazz, ConstructorUtils.invokeConstructor(clazz));
                }

                bucketMap.put(key, bucket);

                initConfiguration(bucket, classList);

                inject(bucket);

                if (bucket.getEnterMethod() == null || bucket.getEnterObj() == null) {
                    throw new PluginException("插件加载失败，未找到插件启动入口");
                }

                bucket.getEnterMethod().invoke(bucket.getEnterObj());
            } else {
                logger.warn("未找到插件加载器");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (PluginException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void inject(Bucket bucket) throws Exception {
        for (Map.Entry<Class, Object> entry : bucket.getConfMap().entrySet()) {
            Class<?> clazz = entry.getKey();
            Object obj = entry.getValue();
            List<Field> fieldList = FieldUtils.getFieldsListWithAnnotation(clazz, Inject.class);
            for (Field field : fieldList) {
                Inject inj = field.getAnnotation(Inject.class);

                BeanKey key = BeanKey.getKey(inj.value(), field.getType());

                String pluginName = inj.pluginName();
                if (Strings.isNullOrEmpty(pluginName)) {
                    pluginName = bucket.getName();
                }
                PluginFactoryBean factoryBean = findFactoryBean(pluginName, key);
                FieldUtils.writeField(field, obj, factoryBean.getObject(), true);
            }
        }
    }

    private void initConfiguration(Bucket bucket, List<Class<?>> classList) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, BeanCreateException {
        Map<BeanKey, MethodBeanDef> methodMap = initMethodDef(bucket, classList);

        initAopProxy(bucket);

        initBeanFactorys(bucket, methodMap);
    }

    private void initAopProxy(Bucket bucket) {
        for (Map.Entry<Class, Object> entry : bucket.getAopMap().entrySet()) {
            Class<?> clazz = entry.getKey();
            Object obj = entry.getValue();

            List<Method> methodList = MethodUtils.getMethodsListWithAnnotation(clazz, Around.class);
            for (Method method : methodList) {
                Around around = method.getAnnotation(Around.class);
                String reg = around.value();
                ProxyBeanDef def = new ProxyBeanDef(reg);
                def.setMethod(method);
                def.setObj(obj);

                List<ProxyBeanDef> defList = bucket.getProxyMethodMap().get(around.value());
                if (defList == null) {
                    defList = new ArrayList<>();
                }
                defList.add(def);

                bucket.getProxyMethodMap().put(around.value(), defList);
            }
        }
    }

    private Map<BeanKey, MethodBeanDef> initMethodDef(Bucket bucket, List<Class<?>> classList) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, BeanCreateException {
        Map<BeanKey, MethodBeanDef> methodMap = new HashMap<>();

        for (Class<?> clazz : classList) {
            Object obj = bucket.getConfMap().get(clazz);

            {
                List<Method> methods = MethodUtils.getMethodsListWithAnnotation(clazz, Bean.class);

                for (Method method : methods) {
                    Class<?> returnType = method.getReturnType();
                    MethodBeanDef def = new MethodBeanDef();
                    def.setObj(obj);
                    def.setMethod(method);
                    Bean bean = method.getAnnotation(Bean.class);
                    BeanKey key = BeanKey.getKey(bean.value(), returnType);

                    methodMap.put(key, def);
                }
            }

            // init aop
            {
                List<Method> methods = MethodUtils.getMethodsListWithAnnotation(clazz, Aop.class);

                for (Method method : methods) {
                    Class<?> aopClazz = method.getReturnType();
                    bucket.getAopMap().put(aopClazz, ConstructorUtils.invokeConstructor(aopClazz));
                }
            }
            {
                List<Method> startList = MethodUtils.getMethodsListWithAnnotation(clazz, ServiceStart.class);
                if ((bucket.getEnterMethod() != null && startList.size() > 0)
                        || (bucket.getEnterMethod() == null && startList.size() > 1)) {
                    throw new BeanCreateException("发现多个插件启动入口，每个插件只能有一个入口");
                }
                if (bucket.getEnterMethod() == null && startList.size() == 1) {
                    bucket.setEnterMethod(startList.get(0));
                    bucket.setEnterObj(obj);
                }
            }

        }
        return methodMap;
    }

    private List<ProxyMethod> isProxyBean(String bucketName, Method method) {
        Bucket bucket = bucketMap.get(bucketName);
        List<ProxyMethod> proxyMethodList = new ArrayList<>();

        for (Map.Entry<String, List<ProxyBeanDef>> entry : bucket.getProxyMethodMap().entrySet()) {
            String key = StringUtils.trimToEmpty(entry.getKey());
            if (key.startsWith("annotation:")) {
                String className = StringUtils.removeStart(key, "annotation:");
                Optional<Class> opt = bucket.getPlugin().loadClass(className);
                if (opt.isPresent()) {
                    List<Method> methodList = MethodUtils.getMethodsListWithAnnotation(method.getReturnType(), opt.get());

                    if (!methodList.isEmpty()) {
                        for (Method m : methodList) {
                            proxyMethodList.add(new ProxyMethod(method, bucket.getProxyMethodMap().get(key), m, opt.get()));
                        }
                    }
                }
            }
            if (key.startsWith("execution:")) {

            }

        }
        return proxyMethodList;
    }

    private void initBeanFactorys(Bucket bucket, Map<BeanKey, MethodBeanDef> methodMap) throws InvocationTargetException, IllegalAccessException {
        for (Map.Entry<BeanKey, MethodBeanDef> entry : methodMap.entrySet()) {
            BeanKey key = entry.getKey();
            MethodBeanDef def = entry.getValue();

            if (!bucket.beanMap.containsKey(key)) {
                PluginFactoryBean factoryBean = null;
                List<ProxyMethod> proxyMethodList = isProxyBean(bucket.name, def.getMethod());
                if (!proxyMethodList.isEmpty()) {
                    factoryBean = new ProxyFactoryBean(bucket.name, bucket.getPlugin().getClassLoader(), key, def, proxyMethodList);
                } else {
                    factoryBean = new PluginFactoryBean(bucket.name, bucket.getPlugin().getClassLoader(), key, def);
                }
                factoryBean.setBeanContext(this);
                bucket.beanMap.put(key, factoryBean);
            }
        }
    }

    private void getImportClass(List<Class<?>> classList, Class<?> clazz) {
        Configuration confAnno = clazz.getAnnotation(Configuration.class);

        if (confAnno != null) {
            classList.add(clazz);
            Import importAnno = clazz.getAnnotation(Import.class);
            if (importAnno != null) {

                for (Class<?> cls : importAnno.value()) {
                    getImportClass(classList, cls);
                }

            }
        }

    }

    public PluginFactoryBean findFactoryBean(String pluginName, BeanKey key) throws CannotFindFactoryBeanException {
        Bucket bucket = this.bucketMap.get(pluginName);
        if (bucket == null) {
            throw new CannotFindFactoryBeanException(MessageFormat.format("找不到指定插件:{0}", pluginName));
        }
        PluginFactoryBean factoryBean = bucket.beanMap.get(key);
        if (factoryBean == null) {
            throw new CannotFindFactoryBeanException(MessageFormat.format("找不到指定插件:{0}", pluginName));
        }
        return factoryBean;

    }

    static class Bucket {
        private String name;
        private int version;

        private Object enterObj;
        private Method enterMethod;

        private Plugin plugin;

        private Map<Class, Object> confMap = new HashMap<>();

        private Map<BeanKey, PluginFactoryBean> beanMap = new HashMap<>();

        private Map<Class, Object> aopMap = new HashMap<>();

        private Map<String, List<ProxyBeanDef>> proxyMethodMap = new HashMap<>();

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getVersion() {
            return version;
        }

        public void setVersion(int version) {
            this.version = version;
        }

        public static String keyMaker(String name, int version) {
            return MessageFormat.format("{0}", name);
//            return MessageFormat.format("{0}_{1}", name, version);
        }

        public Object getEnterObj() {
            return enterObj;
        }

        public void setEnterObj(Object enterObj) {
            this.enterObj = enterObj;
        }

        public Method getEnterMethod() {
            return enterMethod;
        }

        public void setEnterMethod(Method enterMethod) {
            this.enterMethod = enterMethod;
        }

        public Map<Class, Object> getConfMap() {
            return confMap;
        }

        public void setConfMap(Map<Class, Object> confMap) {
            this.confMap = confMap;
        }

        public Map<Class, Object> getAopMap() {
            return aopMap;
        }

        public Map<String, List<ProxyBeanDef>> getProxyMethodMap() {
            return proxyMethodMap;
        }

        public Plugin getPlugin() {
            return plugin;
        }

        public void setPlugin(Plugin plugin) {
            this.plugin = plugin;
        }
    }
}
